package engine

import (
	"encoding/json"
	"strconv"
	"strings"

	"github.com/pkg/errors"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"

	dec "github.com/Checkmarx/kics/v2/pkg/detector"
	"github.com/Checkmarx/kics/v2/pkg/engine/similarity"
	"github.com/Checkmarx/kics/v2/pkg/model"
)

const (
	formatFloat64   = 64
	searchKeyMinLen = 3
)

func modifyVulSearchKeyReference(doc interface{}, originalSearchKey string, stringVulList []string) (string, bool) {
	for index, vulSplit := range stringVulList {
		switch docTyped := doc.(type) {
		case map[string]interface{}:
			if strings.HasPrefix(vulSplit, "{{") && strings.HasSuffix(vulSplit, "}}") {
				vulSplit = vulSplit[2 : len(vulSplit)-2]
			}
			if vulSplitEqual := strings.Split(vulSplit, "="); len(vulSplitEqual) != 1 {
				vulSplit = vulSplitEqual[0]
			}
			newDoc, foundEntry := docTyped[vulSplit]
			if metadataRefDoc, ok := docTyped["RefMetadata"]; ok && foundEntry && index < len(stringVulList) {
				newSearchKey := strings.Join(stringVulList[:index], ".") + ".$ref=" + (metadataRefDoc.(map[string]interface{})["$ref"].(string))
				return newSearchKey, true
			} else if foundEntry {
				doc = newDoc
			} else {
				return originalSearchKey, false
			}
		case []interface{}:
			for _, listDoc := range docTyped {
				if newSearchKey, modified := modifyVulSearchKeyReference(listDoc, originalSearchKey, stringVulList[index:]); modified {
					return strings.Join(stringVulList[:index], ".") + "." + newSearchKey, true
				}
			}
			return originalSearchKey, false
		default:
			if index != len(stringVulList)-1 {
				return originalSearchKey, false
			}
		}
	}
	return originalSearchKey, false
}

// DefaultVulnerabilityBuilder defines a vulnerability builder to execute default actions of scan
//
//nolint:gocyclo
var DefaultVulnerabilityBuilder = func(ctx *QueryContext,
	tracker Tracker,
	v interface{},
	detector *dec.DetectLine,
	useOldSeverities bool,
	kicsComputeNewSimID bool,
	kicsMigrationQueryInfo map[string]TransitionQueryInfo,
) (*model.Vulnerability, error) {
	vObj, ok := v.(map[string]interface{})
	if !ok {
		return &model.Vulnerability{}, ErrInvalidResult
	}

	vObj = mergeWithMetadata(vObj, ctx.Query.Metadata.Metadata)

	var err error
	var output []byte

	output, err = json.Marshal(vObj)
	if err != nil {
		return &model.Vulnerability{}, errors.Wrap(err, "failed to marshall query output")
	}

	var fileID *string

	fileID, err = mapKeyToString(vObj, "documentId", false)
	if err != nil {
		return &model.Vulnerability{}, errors.Wrap(err, "failed to recognize file id")
	}

	file, ok := ctx.Files[*fileID]
	if !ok {
		return &model.Vulnerability{}, errors.New("failed to find file from query response")
	}

	logWithFields := log.With().
		Str("scanID", ctx.scanID).
		Str("fileName", file.FilePath).
		Str("queryName", ctx.Query.Metadata.Query).
		Logger()

	detector.SetupLogs(&logWithFields)

	linesVulne := model.VulnerabilityLines{
		Line:      -1,
		VulnLines: &[]model.CodeLine{},
	}

	initialSearchKeyValue := ""
	searchKey := ""
	if s, ok := vObj["searchKey"]; ok {
		searchKey = s.(string)
		initialSearchKeyValue = searchKey
		intDoc := file.LineInfoDocument
		vulsSplit := strings.Split(searchKey, ".")

		if file.Kind == model.KindINI {
			vulsSplit, searchKey = sanitizeINIKey(vulsSplit)
		}

		if strings.Contains(vulsSplit[len(vulsSplit)-1], "RefMetadata") {
			return &model.Vulnerability{}, ErrNoResult
		}

		// modify the search key in cases where it should be referencing a ref instead of part of the resolved object
		searchKey, _ = modifyVulSearchKeyReference(intDoc, searchKey, vulsSplit)
		vObj["searchKey"] = searchKey
		linesVulne = detector.DetectLine(&file, searchKey, &logWithFields)
	} else {
		logWithFields.Error().Msg("Saving result. failed to detect line")
	}

	lineNumber := 0
	var oldSearchLineOutput = initialSearchKeyValue // the line number of the gjson query
	var newSearchLineOutput = initialSearchKeyValue // the array used to create the gjson query
	searchLineCalc := &searchLineCalculator{
		lineNr:                   -1,
		vObj:                     vObj,
		file:                     file,
		detector:                 detector,
		oldSearchLineOutput:      initialSearchKeyValue,
		newSearchLineOutput:      initialSearchKeyValue,
		vulnerabilityLines:       linesVulne, // in case the searchLine is not defined, we will use the computed via DetectLine
		usingComputeSimilarityID: kicsComputeNewSimID,
	}

	if file.Kind != model.KindHELM && len(file.ResolvedFiles) == 0 {
		// calculate search Line if possible (default uses values of the search key)
		lineNumber, oldSearchLineOutput, newSearchLineOutput, linesVulne = calculateSearchLine(searchLineCalc)
	} else {
		// get only searchLine string value for similarity ID computation
		_, newSearchLineOutput = calculateSearchLineWithoutGJson(searchLineCalc)
	}

	if linesVulne.Line == -1 {
		logWithFields.Warn().Msgf("Failed to detect line with result searchLine, query response %s", searchKey)
		linesVulne.Line = 1
	}

	searchValue := ""
	if s, ok := vObj["searchValue"]; ok {
		searchValue = s.(string)
	}

	overrideKey := ""
	if s, ok := vObj["overrideKey"]; ok {
		overrideKey = s.(string)
	}

	queryID := getStringFromMap("id", DefaultQueryID, overrideKey, vObj, &logWithFields)

	severity := getResolvedSeverity(vObj, &logWithFields, overrideKey, useOldSeverities)

	issueType := DefaultIssueType
	if v := mustMapKeyToString(vObj, "issueType"); v != nil {
		issueType = model.IssueType(*v)
	}

	similarityID, oldSimilarityID := generateSimilaritiesID(ctx, linesVulne.ResolvedFile, strconv.Itoa(file.SubDocumentIndex),
		queryID, newSearchLineOutput, searchValue, searchKey, oldSearchLineOutput, kicsComputeNewSimID, &logWithFields,
		tracker, kicsMigrationQueryInfo)

	return &model.Vulnerability{
		ID:               0,
		SimilarityID:     PtrStringToString(similarityID),
		OldSimilarityID:  PtrStringToString(oldSimilarityID),
		ScanID:           ctx.scanID,
		FileID:           file.ID,
		FileName:         linesVulne.ResolvedFile,
		QueryName:        getStringFromMap("queryName", DefaultQueryName, overrideKey, vObj, &logWithFields),
		QueryID:          queryID,
		Experimental:     getBoolFromMap("experimental", DefaultExperimental, overrideKey, vObj, &logWithFields),
		QueryURI:         getStringFromMap("descriptionUrl", DefaultQueryURI, overrideKey, vObj, &logWithFields),
		Category:         getStringFromMap("category", "", overrideKey, vObj, &logWithFields),
		Description:      getStringFromMap("descriptionText", "", overrideKey, vObj, &logWithFields),
		DescriptionID:    getStringFromMap("descriptionID", DefaultQueryDescriptionID, overrideKey, vObj, &logWithFields),
		Severity:         severity,
		Platform:         getStringFromMap("platform", "", overrideKey, vObj, &logWithFields),
		CWE:              ctx.Query.Metadata.CWE,
		RiskScore:        ctx.Query.Metadata.RiskScore,
		Line:             linesVulne.Line,
		VulnLines:        linesVulne.VulnLines,
		ResourceType:     PtrStringToString(mustMapKeyToString(vObj, "resourceType")),
		ResourceName:     PtrStringToString(mustMapKeyToString(vObj, "resourceName")),
		IssueType:        issueType,
		SearchKey:        searchKey,
		SearchLine:       lineNumber,
		SearchValue:      searchValue,
		KeyExpectedValue: PtrStringToString(mustMapKeyToString(vObj, "keyExpectedValue")),
		KeyActualValue:   PtrStringToString(mustMapKeyToString(vObj, "keyActualValue")),
		Value:            mustMapKeyToString(vObj, "value"),
		Output:           string(output),
		CloudProvider:    getCloudProvider(overrideKey, vObj, &logWithFields),
		Remediation:      PtrStringToString(mustMapKeyToString(vObj, "remediation")),
		RemediationType:  PtrStringToString(mustMapKeyToString(vObj, "remediationType")),
	}, nil
}

func generateSimilaritiesID(ctx *QueryContext, resolvedFile, subDocumentIdx, queryID, newSimilarityIDLineInfo, searchValue,
	searchKey, oldSimilarityIDLineInfo string, kicsComputeNewSimID bool, logWithFields *zerolog.Logger, tracker Tracker,
	kicsMigrationQueryInfo map[string]TransitionQueryInfo,
) (similarityID, oldSimilarityID *string) {
	transitionType := checkQueryTransitionType(kicsMigrationQueryInfo, queryID, ctx.Query.Metadata.Experimental)
	// only will generate pair of similarity IDs if the scan flag is enabled and the transition type is not YetToBeChecked
	// or if the scan flag is disabled and the transition type is NonGracefullyTransition
	if kicsComputeNewSimID && transitionType != YetToBeChecked ||
		!kicsComputeNewSimID && transitionType == NonGracefullyTransition {
		newSimilarityID, err := buildSimilarityID(ctx, resolvedFile, subDocumentIdx, queryID, newSimilarityIDLineInfo, searchValue)
		if err != nil {
			logWithFields.Err(err).Send()
			tracker.FailedComputeSimilarityID()
		}

		oldSimilarityID, err = oldBuildSimilarityID(ctx, resolvedFile, queryID, searchKey, oldSimilarityIDLineInfo,
			searchValue, transitionType)
		if err != nil {
			logWithFields.Err(err).Send()
			tracker.FailedComputeOldSimilarityID()
		}

		if oldSimilarityID != nil {
			if oldSimilarityID == newSimilarityID {
				logWithFields.Warn().Msgf("SimilarityIDs %s are duplicated for query %s, resolvedFile %s, searchKey %s, searchValue %s"+
					" newSimilarityIDLineInfo: %s, oldSimilarityIDLineInfo: %s", PtrStringToString(newSimilarityID), queryID,
					resolvedFile, searchKey, searchValue, newSimilarityIDLineInfo, oldSimilarityIDLineInfo)
			} else {
				logWithFields.Info().Msgf("SimilarityID transition for query %s, resolvedFile %s, searchKey %s, searchValue %s,"+
					" newSimilarityIDLineInfo: %s, oldSimilarityIDLineInfo: %s, newSimilarityID: %s, oldSimilarityID: %s", queryID,
					resolvedFile, searchKey, searchValue, newSimilarityIDLineInfo, oldSimilarityIDLineInfo,
					PtrStringToString(newSimilarityID), PtrStringToString(oldSimilarityID))
			}
		}

		return newSimilarityID, oldSimilarityID
	} else {
		newSimilarityID, err := oldBuildSimilarityID(ctx, resolvedFile, queryID, searchKey, oldSimilarityIDLineInfo,
			searchValue, transitionType)
		if err != nil {
			logWithFields.Err(err).Send()
			tracker.FailedComputeSimilarityID()
		}
		return newSimilarityID, oldSimilarityID
	}
}

func buildSimilarityID(
	ctx *QueryContext,
	resolvedFile,
	subDocumentIdx,
	queryID,
	similarityIDLineInfo,
	searchValue string) (*string, error) {
	return similarity.ComputeSimilarityID(ctx.BaseScanPaths, resolvedFile, subDocumentIdx, queryID, similarityIDLineInfo, searchValue)
}

func oldBuildSimilarityID(ctx *QueryContext, resolvedFile, queryID, searchKey, similarityIDLineOrSearchKey, searchValue string,
	transitionType VulnerabilityBuilderTransition,
) (*string, error) {
	// prepare fields based on the transition type
	switch transitionType {
	case AddedSearchValue:
		searchValue = "" // remove search value for old similarity ID computation
	case AddedSearchLine:
		similarityIDLineOrSearchKey = searchKey // make sure we use the search key as the line info
	case AddedSearchValueAndAddedSearchLine:
		similarityIDLineOrSearchKey = searchKey // make sure we use the search key as the line info
		searchValue = ""                        // remove search value for old similarity ID computation
	case TransitionWithoutChanges, YetToBeChecked:
		// YetToBeChecked should only reach this point if the scan flag is disabled, so we will only compute the old similarity ID
		// TransitionWithoutChanges will use the searchKey or searchLine number as the similarity ID line info
	case NonGracefullyTransition:
		// This is a non-graceful transition, so we will not compute the old similarity ID
		return nil, nil
	}

	if checkMinified(ctx, resolvedFile) {
		return similarity.ComputeSimilarityID(ctx.BaseScanPaths, resolvedFile, "", queryID, searchKey, searchValue)
	} else {
		return similarity.ComputeSimilarityID(ctx.BaseScanPaths, resolvedFile, "", queryID, similarityIDLineOrSearchKey, searchValue)
	}
}

func checkMinified(ctx *QueryContext, resolvedFile string) bool {
	for i := range ctx.Files {
		if ctx.Files[i].FilePath == resolvedFile {
			return ctx.Files[i].IsMinified
		}
	}
	return false
}

func getCloudProvider(overrideKey string, vObj map[string]interface{}, logWithFields *zerolog.Logger) string {
	cloudProvider := ""
	if _, ok := vObj["cloudProvider"]; ok {
		cloudProvider = getStringFromMap("cloudProvider", "", overrideKey, vObj, logWithFields)
	}
	return cloudProvider
}

// calculate search Line if possible (default uses values of the search key)
func calculateSearchLine(searchLineCalc *searchLineCalculator) (lineNumber int,
	similarityIDLineInfoOld, similarityIDLineInfoNew string, linesVulnerability model.VulnerabilityLines) {
	searchLineCalc.calculate()
	return searchLineCalc.lineNr, searchLineCalc.oldSearchLineOutput, searchLineCalc.newSearchLineOutput, searchLineCalc.vulnerabilityLines
}

func calculateSearchLineWithoutGJson(searchLineCalc *searchLineCalculator) (oldSearchLineOutput, newSearchLineOutput string) {
	searchLineCalc.generateSearchLineOutputWithoutGJson()
	return searchLineCalc.oldSearchLineOutput, searchLineCalc.newSearchLineOutput
}

func getResolvedSeverity(vObj map[string]interface{}, logWithFields *zerolog.Logger,
	overrideKey string, useOldSeverities bool) model.Severity {
	var severity model.Severity = model.SeverityInfo
	s, err := mapKeyToString(vObj, "severity", false)

	if err == nil {
		sev := getSeverity(strings.ToUpper(*s))
		if sev == "" {
			logWithFields.Warn().Str("severity", *s).Msg("Saving result. invalid severity constant value")
		} else {
			severity = sev
			overrideValue := tryOverride(overrideKey, "severity", vObj)
			if overrideValue != nil {
				sev = getSeverity(strings.ToUpper(*overrideValue))
				if sev != "" {
					severity = sev
				}
			} else if useOldSeverities {
				oldS, errOld := mapKeyToString(vObj, "oldSeverity", false)
				if errOld == nil {
					oldSev := getSeverity(strings.ToUpper(*oldS))
					severity = oldSev
				}
			}
		}
	} else {
		logWithFields.Info().Msg("Saving result. failed to detect severity")
	}
	return severity
}

// sanitizeINIKey removes useless searchkey elements like "all" and "children"
func sanitizeINIKey(vulsSplit []string) (vulsRefact []string, searchKey string) {
	length := len(vulsSplit)
	vulsRefact = vulsSplit
	if length >= searchKeyMinLen {
		vulsRefact = []string{"[" + vulsSplit[2] + "]"}

		if length >= searchKeyMinLen+2 {
			vulsRefact = append(vulsRefact, vulsSplit[4])

			if length >= searchKeyMinLen+4 {
				vulsRefact = append(vulsRefact, vulsSplit[6])
			}
		}
	}
	return vulsRefact, strings.Join(vulsRefact, ".")
}
